﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Array2DExtensions
{
    /// <summary>
    /// Adds functionality to 2D T arrays
    /// Stanislav Radkov®  2012 :D 
    /// </summary>
    public static class Array2DExtensions
    {
        /// <summary>
        /// Determines whether a cell has a valid index which is in the boundaries of the array.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="array">The array.</param>
        /// <param name="cellIndex">Index of the cell.</param>
        /// <returns>
        ///   <c>true</c>if the index is valid otherwise<c>false</c>.
        /// </returns>
        public static bool IsInBoundary<T>(this T[,] array, Index cellIndex)
        {
            bool isRowValid = cellIndex.Row >= 0 && cellIndex.Row < array.GetLength(0);
            bool isColValid = cellIndex.Col >= 0 && cellIndex.Col < array.GetLength(1);

            return isRowValid && isColValid;
        }

        /// <summary>
        /// Initializes the array with the given value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        public static void Init<T>(this T[,] array, T value)
        {
            for (int row = 0; row < array.GetLength(0); row++)
            {
                for (int col = 0; col < array.GetLength(1); col++)
                {
                    array[row, col] = value;
                }
            }
        }

        /// <summary>
        /// Gets the value with the given index
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="array">The array.</param>
        /// <param name="cellIndex">Index of the cell.</param>
        /// <returns>The value in the given cell</returns>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static T GetValue<T>(this T[,] array, Index cellIndex)
        {
            if (!array.IsInBoundary(cellIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", cellIndex, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            T value = array[cellIndex.Row, cellIndex.Col];
            return value;
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cell">The cell.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static void SetValue<T>(this T[,] array, Cell<T> cell) where T : IComparable<T>
        {
            if (!array.IsInBoundary(cell as Index))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", cell, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            array[cell.Row, cell.Col] = cell.Value;
        }

        /// <summary>
        /// Sets the value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cell">The cell.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static void SetValue<T>(this T[,] array, Index index, T value)
        {
            if (!array.IsInBoundary(index))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", index, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            array[index.Row, index.Col] = value;
        }

        /// <summary>
        /// Checks whether a rows contains a given value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="row">The row.</param>
        /// <param name="value">The value.</param>
        public static bool RowContains<T>(this T[,] array, Int32 row, T value) where T : IComparable
        {
            bool contains = false;

            for (int col = 0; col < array.GetLength(1); col++)
            {
                if (array[row, col].CompareTo(value) == 0)
                {
                    contains = true;
                    break;
                }
            }

            return contains;
        }

        /// <summary>
        /// Checks whether a column contains a given value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="col">The col.</param>
        /// <param name="value">The value.</param>
        /// <returns></returns>
        public static bool ColContains<T>(this T[,] array, Int32 col, T value) where T : IComparable
        {
            bool contains = false;

            for (int row = 0; row < array.GetLength(0); row++)
            {
                if (array[row, col].CompareTo(value) == 0)
                {
                    contains = true;
                    break;
                }
            }

            return contains;
        }

        /// <summary>
        /// Searches for a value in a given direction
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        /// <param name="startIndex">The start index.</param>
        /// <param name="direction">The direction.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static bool SearchInDirections<T>(this T[,] array, T value, Index startIndex, Index direction) where T : IComparable
        {
            if (!array.IsInBoundary(startIndex))
            {
                throw new IndexOutOfRangeException("The given index is not in the boundaries of the array!");
            }

            bool found = false;
            Index currentPosition = startIndex;

            while (array.IsInBoundary(currentPosition))
            {
                if (array.GetValue(currentPosition).CompareTo(value) == 0)
                {
                    found = true;
                    break;
                }
                else
                {
                    currentPosition += direction;
                }
            }

            return found;
        }

        /// <summary>
        /// Returns the indexes of the cells which contain the given value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        public static List<Index> IndexesOf<T>(this T[,] array, T value)
        {
            List<Index> cellsContainingValue = new List<Index>();

            for (int row = 0; row < array.GetLength(0); row++)
            {
                for (int col = 0; col < array.GetLength(1); col++)
                {
                    if (array[row, col].Equals(value))
                    {
                        cellsContainingValue.Add(new Index(row, col));
                    }
                }
            }

            return cellsContainingValue;
        }

        /// <summary>
        /// Returns the index of the first found cells which contain the given value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        public static Index IndexOf<T>(this T[,] array, T value)
        {
            Index cellContainingValue = null;

            for (int row = 0; row < array.GetLength(0); row++)
            {
                for (int col = 0; col < array.GetLength(1); col++)
                {
                    if (array[row, col].Equals(value))
                    {
                        cellContainingValue = new Index(row, col);
                        break;
                    }
                }
            }

            return cellContainingValue;
        }

        /// <summary>
        /// Finds the maximum value in the given row
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="row">The row.</param>
        public static T RowMax<T>(this T[,] array, Int32 row) where T : IComparable
        {
            T maxValueInRow = array[row, 0];

            for (int col = 0; col < array.GetLength(1); col++)
            {
                if (array[row, col].CompareTo(maxValueInRow) > 0)
                    maxValueInRow = array[row, col];
            }

            return maxValueInRow;
        }

        /// <summary>
        /// Finds the maximum value in the given column
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="col">The col.</param>
        public static T ColMax<T>(this T[,] array, Int32 col) where T : IComparable
        {
            T maxValueInCol = array[0, col];

            for (int row = 0; row < array.GetLength(0); row++)
            {
                if (array[row, col].CompareTo(maxValueInCol) < 0)
                    maxValueInCol = array[row, col];
            }

            return maxValueInCol;
        }
        
        /// <summary>
        /// Collects all the items in the given row
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="row">The row.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell<T>> RowToList<T>(this T[,] array, Int32 row) where T : IComparable<T>
        {
            if (!array.IsInBoundary(new Index(row, 0)))
            {
                string message = string.Format("The given row index is not in the boundaries of the array! Given row:{0} , Max Row index in the array:{1}", row, array.GetLength(0));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell<T>> rowElements = new List<Cell<T>>();
            Index currentIndex = new Index(row, 0);

            while (array.IsInBoundary(currentIndex))
            {
                Cell<T> element = new Cell<T>(array.GetValue(currentIndex), currentIndex);
                rowElements.Add(element);

                currentIndex += Index.Right;
            }

            return rowElements;
        }

        /// <summary>
        /// Collects all the items in the given column
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="col">The column</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell<T>> ColToList<T>(this T[,] array, Int32 col) where T : IComparable<T>
        {
            if (!array.IsInBoundary(new Index(0, col)))
            {
                string message = string.Format("The given column index is not in the boundaries of the array! Given col:{0} , Max Col index in the array:{1}", col, array.GetLength(1));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell<T>> colElements = new List<Cell<T>>();
            Index currentIndex = new Index(0, col);

            while (array.IsInBoundary(currentIndex))
            {
                Cell<T> element = new Cell<T>(array.GetValue(currentIndex), currentIndex);
                colElements.Add(element);

                currentIndex += Index.Down;
            }

            return colElements;
        }

        /// <summary>
        /// Collects all the items from a start index in a given direction
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <param name="direction">The direction.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell<T>> FromStartToEndInDesiredDirectionToList<T>(this T[,] array, Index startIndex, Index direction) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell<T>> elements = new List<Cell<T>>();
            Index currentIndex = startIndex;

            while (array.IsInBoundary(currentIndex))
            {
                Cell<T> element = new Cell<T>(array.GetValue(currentIndex), currentIndex);
                elements.Add(element);

                currentIndex += direction;
            }

            return elements;
        }

        /// <summary>
        /// Gets the straight neighbours. Top/Down and Left/Right 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell<T>> GetStraightNeighbours<T>(this T[,] array, Index startIndex) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell<T>> neighbours = new List<Cell<T>>();
            Index currentIndex;

            foreach (Index direction in Index.StraightDirections)
            {
                currentIndex = startIndex + direction;

                if (array.IsInBoundary(currentIndex))
                    neighbours.Add(new Cell<T>(array.GetValue(currentIndex), currentIndex));
            }

            return neighbours;
        }

        /// <summary>
        /// Gets all the neighbours of the given cell
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell<T>> GetStraightAndDiagonalNeighbours<T>(this T[,] array, Index startIndex) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index(array.GetLength(0), array.GetLength(1)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell<T>> neighbours = new List<Cell<T>>();
            Index currentIndex;

            foreach (Index direction in Index.StraightAndDiagonalDirections)
            {
                currentIndex = startIndex + direction;

                if (array.IsInBoundary(currentIndex))
                    neighbours.Add(new Cell<T>(array.GetValue(currentIndex), currentIndex));
            }

            return neighbours;
        }
    }
}
